其他
撸了一个「合成大西瓜」
最近「合成大西瓜」这个游戏也很火热,玩了一阵还挺有意思的。研究了一下原理,发现目前流传的版本都是魔改编译后的版本,代码经过压缩不具备可读性,因此决定自己照着实现一个。这个游戏已经开源,获取项目可以关注微信公众号「逛逛GitHub」回复「大西瓜」获取下载链接。
01
游戏逻辑
在生成一个水果 点击屏幕,水果移动到对应x轴位置并自由下落 每个水果会与其他水果发生碰撞,两个相同的水果碰撞时会发生合并,升级成更高一级的水果
生成水果 水果下落与碰撞 水果消除动画效果及升级逻辑
02
预备工作
cocos creator 基本概念
11张水果贴图 每种水果合成效果贴图,均包含 一张果粒图片 一张圆形水珠图片 一张爆炸贴图 两个西瓜合成时有灯光和撒花的效果,时间有限暂不实现
.mp3
后缀的请求快速筛选对应资源。水果消除时的爆炸声和水声
03
创建游戏场景和背景
Scene
,取名 game 作为游戏主场景:960*640
,可以选择根节点然后再右侧属性检查器中调整宽高为 640*960
#FBE79D
的,因此使用一个单色 Sprite 填充即可0.5*0.5
,此时整个画布会被完全填充。Game.js
,creator 会生成一个带基础 cc.Class
的模板文件Game
组件// Game.js
cc.Class({
extends: cc.Component,
properties: {
},
onLoad(){
},
start(){ }
})
我们需要在这里维护整个游戏的逻辑,后面逐步添加代码内容。
03
Prefab
来控制,制作prefab最简单的方式就是将资源从资源管理器拖动到场景编辑器中,然后再将层级管理器中的节点拖回资源管理器。Game.js
,添加一个属性 fruitPrefab
,其类型为 cc.Prefab。
// Game.js
properties: {
fruitPrefab: {
default: null,
type: cc.Prefab
},
}
Game
组件栏目看见和修改该属性了。我们将刚才制作的 prefab 资源从资源管理器拖动到这里,在初始化的时候,有 cocos 负责初始化对应的属性数据创建单个水果
// Game.js
onLoad(){
let fruit = cc.instantiate(this.fruitPrefab);
fruit.setPosition(cc.v2(0, 400));
this.node.addChild(fruit);
}
Fruit
脚本组件,虽然目前看起来还没有什么用,创建Fruit脚本组件与上面创建Game组件类似,然后选择刚才制作的prefab重新编辑,关联上Fruit用户脚本组件即可。id
和iconSF
const FruitItem = cc.Class({
name: 'FruitItem',
properties: {
id: 0, // 水果的类型
iconSF: cc.SpriteFrame // 贴图资源
}
});
Game
脚本组件新增一个fruits
属性,用于保存每种水果的配置信息,其类型是数组,数组内元素类型为刚才创建的FruitItem
// Game.js
properties: {
fruits: {
default: [],
type: FruitItem
},
}
Fruits
属性,将其长度修改为11,然后依次编写每个水果的id,同时将其贴图资源从资源编辑器贴过来(体力活)Fruit
组件中,我们暴露一个init接口出来:// Fruit.js
properties: {
id: 0,
},
// 实例放在可以在其他组件中调用
init(data) {
this.id = data.id
// 根据传入的参数修改贴图资源
const sp = this.node.getComponent(cc.Sprite)
sp.spriteFrame = data.iconSF
},
// Game.js
createOneFruit(num) {
let fruit = cc.instantiate(this.fruitPrefab);
// 获取到配置信息
const config = this.fruits[num - 1]
// 获取到节点的Fruit组件并调用实例方法
fruit.getComponent('Fruit').init({
id: config.id,
iconSF: config.iconSF
});
}
Game
脚本组件中:onLoad() {
// 监听点击事件
this.node.on(cc.Node.EventType.TOUCH_START, this.onTouchStart, this)
},
onTouchStart(){
this.createOneFruit(1) // 生成水果
}
const instance = cc.director.getPhysicsManager()
instance.enabled = true
// instance.debugDrawFlags = 4
instance.gravity = cc.v2(0, -960);
const collisionManager = cc.director.getCollisionManager();
collisionManager.enabled = true
// 设置四周的碰撞区域
let width = this.node.width;
let height = this.node.height;
let node = new cc.Node();
let body = node.addComponent(cc.RigidBody);
body.type = cc.RigidBodyType.Static;
const _addBound = (node, x, y, width, height) => {
let collider = node.addComponent(cc.PhysicsBoxCollider);
collider.offset.x = x;
collider.offset.y = y;
collider.size.width = width;
collider.size.height = height;
}
_addBound(node, 0, -height / 2, width, 1);
_addBound(node, 0, height / 2, width, 1);
_addBound(node, -width / 2, 0, 1, height);
_addBound(node, width / 2, 0, 1, height);
node.parent = this.node;
Enabled Contact Listener
,这样可以接收到碰撞之后的回调// Fruit.js
onBeginContact(contact, self, other) {
// 检测到是两个相同水果的碰撞
if (self.node && other.node) {
const s = self.node.getComponent('Fruit')
const o = other.node.getComponent('Fruit')
if (s && o && s.id === o.id) {
self.node.emit('sameContact', {self, other});
}
}
},
Game.js
,这样可以在初始化水果的时候注册 sameContact
自定义事件的处理方法:// Game.js
createOneFruit(num) {
let fruit = cc.instantiate(this.fruitPrefab);
// ...其他初始化逻辑
fruit.on('sameContact', ({self, other}) => {
// 两个node都会触发,临时处理,看看有没有其他方法只展示一次的
other.node.off('sameContact')
// 处理水果合并的逻辑,下面再处理
this.onSameFruitContact({self, other})
})
}
这样当水果发生碰撞时,我们就能够监听并处理消除升级逻辑了。
无动画版本
self.node.removeFromParent(false)
other.node.removeFromParent(false)
const {x, y} = other.node // 获取合并的水果位置
const id = other.getComponent('Fruit').id
const nextId = id + 1
const newFruit = this.createFruitOnPos(x, y, nextId) // 在指定位置生成新的水果
碰撞水果向原水果中心移动 果粒爆炸的粒子效果 水珠爆炸的粒子效果 一滩果汁的缩放动画
JuiceItem
,保存单种水果爆炸需要的素材:// Game.js
const JuiceItem = cc.Class({
name: 'JuiceItem',
properties: {
particle: cc.SpriteFrame, // 果粒
circle: cc.SpriteFrame, // 水珠
slash: cc.SpriteFrame, // 果汁
}
});
juices
属性:// Game.js
properties: {
juices: {
default: [],
type: JuiceItem
},
juicePrefab: {
default: null,
type: cc.Prefab
},
}
juices
属性下Juice
脚本,然后记得将该预制资源挂载到 Game 的 juicePrefab
上。Juice
组件,用来实现爆炸的动画逻辑,同样需要暴露 init 接口// Juice.js
cc.Class({
extends: cc.Component,
properties: {
particle: {
default: null,
type: cc.SpriteFrame
},
circle: {
default: null,
type: cc.SpriteFrame
},
slash: {
default: null,
type: cc.SpriteFrame
}
},
// 同样暴露一个init接口
init(data) {
this.particle = data.particle
this.circle = data.particle
this.slash = data.slash
},
// 动画效果
showJuice(){
}
}
// Game.js
let juice = cc.instantiate(this.juicePrefab);
this.node.addChild(juice);
const config = this.juices[id - 1]
const instance = juice.getComponent('Juice')
instance.init(config)
instance.showJuice(pos, n) // 对应的爆炸逻辑
showJuice(pos, width) {
// 果粒
for (let i = 0; i < 10; ++i) {
const node = new cc.Node('Sprite');
const sp = node.addComponent(cc.Sprite);
sp.spriteFrame = this.particle;
node.parent = this.node;
// ... 一堆随机的参数
node.position = pos;
node.runAction(
cc.sequence(
// ...各种action对应的动画逻辑
cc.callFunc(function () {
// 动画结束后消除粒子
node.active = false
}, this))
)
}
// 水珠
for (let f = 0; f < 20; f++) {
// 同果粒,使用的spriteFrame切换成 this.circle
}
// 果汁只有一张贴图,使用this.slash,展示常规的action缩放和透明动画即可
},
createFruitL
这个方法来处理爆炸动画,虽然经过了代码压缩,但依稀能看出对应的动画参数逻辑,如果不想调整动画参数,可以借鉴一下:音效
cc.audioEngine
直接播放 AudioClip
资源来实现音效,在 Game 组件下新增两个类型为 AudioClip 的资源,方便脚本组件访问:properties: {
boomAudio: {
default: null,
type: cc.AudioClip
},
waterAudio: {
default: null,
type: cc.AudioClip
}
}
onSameFruitContact(){
cc.audioEngine.play(this.boomAudio, false, 1);
cc.audioEngine.play(this.waterAudio, false, 1);
}
这样就可以在碰撞的时候听到声音了。
完成整个游戏的开发之后,可以选择构建发布,打包成 web-mobile 版本,然后部署在服务器上,就可以给其他人快乐地玩耍了
09
小结
每天推荐一个有趣、好玩且可能你会用到的 GitHub 项目。